#include <assert.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/errno.h>
#include <sys/wait.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <memory.h>
#include <unistd.h>

#ifndef NOB_H_
#define NOB_H_

// We define a lot of functions to be macros so that
// we don't have to deal with argument types.

typedef enum
{
    NOB_INFO,
    NOB_WARNING,
    NOB_ERROR,
    NOB_NO_LOGS
} Nob_Log_Level;

extern Nob_Log_Level NOB_MINIMAL_LOG_LEVEL;

void nob_log(Nob_Log_Level level, const char *format, ...) __attribute__((format(printf, 2, 3)));

#define nob_todo(message)                                                  \
    do                                                                     \
    {                                                                      \
        fprintf(stderr, "%s:%d: TODO: %s\n", __FILE__, __LINE__, message); \
        abort();                                                           \
    } while (0)

#define nob_unreachable(message)                                                  \
    do                                                                            \
    {                                                                             \
        fprintf(stderr, "%s:%d: Unreachable: %s\n", __FILE__, __LINE__, message); \
        abort();                                                                  \
    } while (0)

#define nob_array_len(array) (sizeof((array)) / sizeof((array)[0]))

#define nob_next_arg(argc, argv) ((argc)--, *(argv)++)

/*
    >>> String Utilities <<<
*/

// 1MB
#define NOB_TEMP_BUF_CAPACITY (1 * 1024 * 1024)

static size_t nob_temp_buf_size = 0;
static char nob_temp_buf[NOB_TEMP_BUF_CAPACITY] = {0};

void *nob_temp_alloc(size_t buf_size);

char *nob_temp_sprintf(const char *format, ...) __attribute__((format(printf, 1, 2)));

/*
    >>> File System Utilities <<<
*/

bool nob_fs_rename(const char *old, const char *new);
bool nob_fs_remove(const char *path);

/*
    >>> Dynamic Array <<<

    - Define a dynamic array with the following field naming convention:
    ```c
    typedef struct {
        your_data_type *items;
        size_t count;
        size_t capacity;
    } my_da;
    ```

    - Functions to work with the dynamic array:
    ```c
    nob_da_realloc(&da, new_capacity);
    nob_da_append(&da, item);
    nob_da_free(&da);
    nob_da_reset(&da);
    ```
*/

#define NOB_DA_INIT_CAP 64

#define nob_da_realloc(da, new_capacity)                                               \
    do                                                                                 \
    {                                                                                  \
        if ((new_capacity) > (da)->capacity)                                           \
        {                                                                              \
            if ((da)->capacity == 0)                                                   \
                (da)->capacity = NOB_DA_INIT_CAP;                                      \
            while ((da)->capacity < (new_capacity))                                    \
                (da)->capacity *= 2;                                                   \
            (da)->items = realloc((da)->items, (da)->capacity * sizeof(*(da)->items)); \
            assert((da)->items != NULL && "Reallocation failed.");                     \
        }                                                                              \
    } while (0)

#define nob_da_append(da, new_item)            \
    do                                         \
    {                                          \
        nob_da_realloc((da), (da)->count + 1); \
        (da)->items[(da)->count++] = (item);   \
    } while (0)

#define nob_da_append_multiple(da, new_items, new_count)                                    \
    do                                                                                      \
    {                                                                                       \
        nob_da_realloc((da), (da)->count + (new_count));                                    \
        memcpy((da)->items + (da)->count, (new_items), (new_count) * sizeof(*(da)->items)); \
        (da)->count += (new_count);                                                         \
    } while (0)

#define nob_da_free(da)          \
    do                           \
    {                            \
        if ((da)->items != NULL) \
        {                        \
            free((da)->items);   \
            (da)->items = NULL;  \
        }                        \
    } while (0)

#define nob_da_reset(da)    \
    do                      \
    {                       \
        nob_da_free((da));  \
        (da)->count = 0;    \
        (da)->capacity = 0; \
    } while (0)

typedef struct
{
    const char **items;
    size_t count;
    size_t capacity;
} Nob_Cmd;

#define nob_cmd_append(cmd, ...)         \
    nob_da_append_multiple(              \
        (cmd),                           \
        ((const char *[]){__VA_ARGS__}), \
        sizeof((const char *[]){__VA_ARGS__}) / sizeof(const char *))

// Executes the given command and automatically resets cmd.
int nob_cmd_execute(Nob_Cmd *cmd);
int nob__cmd_execute(Nob_Cmd *cmd);

/*
    >>> nob_go_rebuild_yourself <<<

    Every time you run nob, it should rebuild itself in order to get the nob binary
    up to date. Thus, the first thing you should do in the main function is to call
    nob_go_rebuild_yourself.
    ```c
    int main(int argc, char **argv)
    {
        nob_go_rebuild_yourself(argc, argv);

        // Your code goes here
    }
    ```

    By calling nob_go_rebuild_yourself, nob first renames ./nob to ./nob.old, then
    uses the c compiler to compile the new executable. After that, nob.old
    file will be deleted and nob will rerun itself (the new executable) to perform
    the rest of your code. This means you only need to "cc" compile nob once, and
    the produced nob executable will take care of everything.
*/

bool nob__need_rebuild(const char *source_path, const char *binary_path);

#define nob_go_rebuild_yourself(argc, argv) nob__go_rebuild_yourself(__FILE__, (argc), (argv))
void nob__go_rebuild_yourself(const char *source_path, int argc, char **argv);

#endif // NOB_H_

#ifdef NOB_IMPLEMENTATION

Nob_Log_Level NOB_MINIMAL_LOG_LEVEL = NOB_INFO;

void nob_log(Nob_Log_Level level, const char *format, ...)
{
    if (level < NOB_MINIMAL_LOG_LEVEL)
        return;

    switch (level)
    {
    case NOB_INFO:
        fprintf(stderr, "[INFO] ");
        break;
    case NOB_WARNING:
        fprintf(stderr, "[WARNING] ");
        break;
    case NOB_ERROR:
        fprintf(stderr, "[ERROR] ");
        break;
    case NOB_NO_LOGS:
        return;
    default:
        nob_unreachable("Unexpected log level provided.");
    }

    va_list args;
    va_start(args, format);
    vfprintf(stderr, format, args);
    va_end(args);
    fprintf(stderr, "\n");
}

void *nob_temp_alloc(size_t buf_size)
{
    // Align the buf_size up to a mutiple of word_size
    size_t word_size = sizeof(uintptr_t);
    size_t size = (buf_size + word_size - 1) / word_size * word_size;

    if (nob_temp_buf_size + size > NOB_TEMP_BUF_CAPACITY)
        return NULL;

    void *ret = &nob_temp_buf[nob_temp_buf_size];
    nob_temp_buf_size += size;
    return ret;
}

char *nob_temp_sprintf(const char *format, ...)
{
    // Get the buffer size that we will need to allocate
    va_list args;
    va_start(args, format);
    int count = vsnprintf(NULL, 0, format, args);
    va_end(args);

    // Check for overflow
    assert(count >= 0);
    char *buf = (char *)nob_temp_alloc(count + 1);
    assert(buf != NULL && "Temporary buffer overflowed.");

    // Write into the temporary buffer
    va_start(args, format);
    vsnprintf(buf, count + 1, format, args);
    va_end(args);
    return buf;
}

bool nob_fs_rename(const char *old, const char *new)
{
    nob_log(NOB_INFO, "Rename %s -> %s.", old, new);
    if (rename(old, new) < 0)
    {
        nob_log(NOB_ERROR, "Could not rename %s -> %s : %s.", old, new, strerror(errno));
        return false;
    }
    return true;
}

bool nob_fs_remove(const char *path)
{
    nob_log(NOB_INFO, "Delete %s.", path);
    if (remove(path) < 0)
    {
        nob_log(NOB_ERROR, "Could not delete %s : %s.", path, strerror(errno));
        return false;
    }
    return true;
}

int nob_cmd_execute(Nob_Cmd *cmd)
{
    int ret = nob__cmd_execute(cmd);
    nob_da_reset(cmd);
    return ret;
}

int nob__cmd_execute(Nob_Cmd *cmd)
{
    printf("[CMD] ");
    for (size_t i = 0; i < cmd->count - 1; i++)
    {
        printf("%s ", cmd->items[i]);
    }
    printf("%s\n", cmd->items[cmd->count - 1]);

    pid_t pid = fork();

    if (pid == 0)
    { // In the child process
        char **args = malloc(sizeof(char *) * (cmd->count + 1));
        for (size_t i = 0; i < cmd->count; i++)
            args[i] = (char *)cmd->items[i];
        args[cmd->count] = NULL;

        execvp(args[0], args);
        perror("[CMD] Execution failed");
        free(args); // Do this only if execvp fails
        return 1;
    }
    else if (pid > 0)
    { // In the parent process
        int status;
        waitpid(pid, &status, 0);
        return WEXITSTATUS(status);
    }
    nob_unreachable("Could not create the child process.");
}

bool nob__need_rebuild(const char *source_path, const char *binary_path)
{
    struct stat source_stat, binary_stat;

    if (stat(source_path, &source_stat))
    {
        nob_log(NOB_ERROR, "Could not stat %s : %s.", source_path, strerror(errno));
        abort();
    }

    if (stat(binary_path, &binary_stat))
    {
        // If the nob binary doesn't exist, rebuild
        if (errno == ENOENT) return true;
        nob_log(NOB_ERROR, "Could not stat %s : %s.", binary_path, strerror(errno));
        abort();
    }
    return difftime(source_stat.st_mtime, binary_stat.st_mtime) > 0;
}

void nob__go_rebuild_yourself(const char *source_path, int argc, char **argv)
{
    const char *binary_path = nob_next_arg(argc, argv);

    if (!nob__need_rebuild(source_path, binary_path))
        return;

    char *old_binary_path = nob_temp_sprintf("%s.old", binary_path);
    if (!nob_fs_rename(binary_path, old_binary_path))
        exit(1);

    Nob_Cmd cmd = {0};
    nob_cmd_append(&cmd, "cc", "-o", binary_path, source_path);
    if (nob_cmd_execute(&cmd))
    {
        nob_log(NOB_ERROR, "Failed to compile the new nob binary. Trying to recover nob.old.");
        nob_fs_rename(old_binary_path, binary_path);
        exit(1);
    }

    // If the compilation succeed, delete nob.old
    nob_fs_remove(old_binary_path);

    // Execute the new binary and exit
    nob_cmd_append(&cmd, binary_path);
    nob_da_append_multiple(&cmd, argv, argc);
    if (nob_cmd_execute(&cmd))
        exit(1);
    exit(0);
}

#endif // NOB_IMPLEMENTATION
